﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Xml.Serialization;
using MFToolkit.Net.XBee;
using SC18IM700;
using SC18IM700.Commands;

namespace LaunchPad.Core
{
    public class Node : INotifyPropertyChanged, INameable, IMappable
    {
        public const int MaxEffects = 8;
        private const byte MaxSignalStrength = 64;
        private const int MaxIdentifierLength = 20;

        private readonly List<IdRef<EffectTrack>> _effectReferences;
        private XBeeAddress16 _address16;
        private XBeeAddress64 _address64;
        private bool _armed;
        private ConnectionState _connectionState;
        private GpioPorts _effectPortStates;
        private ObservableCollection<EffectTrack> _effects;
        private Field _field;
        private bool _hovered;
        private byte _lastFinalHopSignalStrength;
        private ObservablePoint _location = new ObservablePoint();
        private string _name;
        private bool _portsConfigured;
        private bool _selected;
        private bool _visible;
        private string _nodeIdentifier;

        public Node()
        {
            Effects = new ObservableCollection<EffectTrack>();
            _effectReferences = new List<IdRef<EffectTrack>>();
        }

        [XmlIgnore]
        public Field Field
        {
            get { return _field; }
            set
            {
                if (_field != value)
                {
                    _field = value;
                    OnPropertyChanged("Field");
                }
            }
        }

        public XBeeAddress64 SerialNumber
        {
            get { return _address64; }
            set
            {
                if (_address64 != value)
                {
                    _address64 = value;
                    OnPropertyChanged("SerialNumber");
                }
            }
        }

        public XBeeAddress16 ShortAddress
        {
            get { return _address16; }
            set
            {
                if (_address16 != value)
                {
                    _address16 = value;
                    OnPropertyChanged("ShortAddress");
                }
            }
        }

        [XmlIgnore]
        public ConnectionState ConnectionState
        {
            get
            {
                if (_connectionState == ConnectionState.Connected && IsArmed)
                    return ConnectionState.Armed;

                return _connectionState;
            }

            set
            {
                if (_connectionState != value)
                {
                    _connectionState = value;

                    //if (value == ConnectionState.Connected)
                    //    Initialize();

                    OnPropertyChanged("ConnectionState");
                }
            }
        }

        [XmlIgnore]
        public byte SignalStrength
        {
            get { return _lastFinalHopSignalStrength; }
            set
            {
                byte strength = Math.Min(MaxSignalStrength, value);

                if (_lastFinalHopSignalStrength != strength)
                {
                    _lastFinalHopSignalStrength = strength;
                    OnPropertyChanged("SignalStrength");
                }
            }
        }

        [XmlIgnore]
        public ObservableCollection<EffectTrack> Effects
        {
            get { return _effects; }

            set
            {
                if (_effects != value)
                {
                    _effects = value;
                    _effects.CollectionChanged += EffectsCollectionChanged;
                    OnPropertyChanged("Effects");
                    //updateLowestReadyCondition();
                }
            }
        }

        [XmlArray("EffectRefs")]
        [XmlArrayItem("EffectRef")]
        public List<IdRef<EffectTrack>> EffectRefXml
        {
            get
            {
                if (Effects.Count == 0)
                    return _effectReferences;
                return Effects.Select(effect => new IdRef<EffectTrack>(effect)).ToList();
            }
        }

        [XmlIgnore]
        public bool IsHovered
        {
            get { return _hovered; }
            set
            {
                if (_hovered != value)
                {
                    _hovered = value;
                    OnPropertyChanged("IsHovered");
                }
            }
        }

        [XmlIgnore]
        public bool IsVisible
        {
            get { return _visible; }
            set
            {
                if (_visible != value)
                {
                    _visible = value;
                    OnPropertyChanged("IsVisible");
                }
            }
        }

        [XmlIgnore]
        public bool IsArmed
        {
            get { return _armed; }
            set
            {
                DIOnMode mode = value
                                    ? DIOnMode.DigitalHi
                                    : DIOnMode.DigitalLo;
                SetXBeePorts(AuxPort.Arm, mode);
                _armed = value;
            }
        }

        [XmlIgnore]
        public bool IsHot
        {
            get
            {
                if (Effects == null)
                    return false;

                return Effects.Any(effect => effect.IsHot);
            }
        }

        #region IMappable Members

        [XmlIgnore]
        public string Description
        {
            get { return "Node " + Name; }
        }

        public ObservablePoint Location
        {
            get { return _location; }
            set
            {
                if (_location != value)
                {
                    _location = value;
                    OnPropertyChanged("Location");
                }
            }
        }

        [XmlIgnore]
        public bool IsSelected
        {
            get { return _selected; }
            set
            {
                if (_selected != value)
                {
                    _selected = value;

                    if (SerialNumber != null && ShortAddress != null)
                    {
                        DIOnMode mode = value
                                            ? DIOnMode.DigitalHi
                                            : DIOnMode.DigitalLo;

                        SetXBeePorts(AuxPort.Identity, mode);
                    }

                    OnPropertyChanged("IsSelected");
                }
            }
        }

        #endregion

        public string NodeIdentifier
        {
            get { return _nodeIdentifier; }
            set
            {
                string safeName = value.Trim();

                if (safeName.Length > MaxIdentifierLength)
                    safeName = safeName.Remove(MaxIdentifierLength);

                if (_nodeIdentifier != safeName)
                {
                    _nodeIdentifier = safeName;
                    SyncRemoteUnitName();
                    OnPropertyChanged("NodeIdentifier");
                }
            }
        }

        #region INameable Members

        [XmlAttribute]
        public string Name
        {
            get { return _name; }
            set
            {
                string safeName = value.Trim();

                if (_name != safeName)
                {
                    _name = safeName;
                    SyncRemoteUnitName();
                    OnPropertyChanged("Name");
                    OnPropertyChanged("Description");
                }
            }
        }

        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        public void UpdateEffects(IEnumerable<EffectTrack> availableEffects)
        {
            if (_effectReferences != null)
            {
                IEnumerable<EffectTrack> effects = from effect in availableEffects
                                                   join effectRef in _effectReferences on effect.Id equals effectRef.Id
                                                   select effect;

                Effects = new ObservableCollection<EffectTrack>(effects);

                foreach (EffectTrack effect in Effects)
                    effect.Node = this;
            }
        }

        public void SetEffectState(EffectPort port, bool on)
        {
            GpioPorts effectPort = GpioPorts.None;

            switch (port)
            {
                case EffectPort.P0:
                    effectPort = GpioPorts.Port0;
                    break;
                case EffectPort.P1:
                    effectPort = GpioPorts.Port1;
                    break;
                case EffectPort.P2:
                    effectPort = GpioPorts.Port2;
                    break;
                case EffectPort.P3:
                    effectPort = GpioPorts.Port3;
                    break;
                case EffectPort.P4:
                    effectPort = GpioPorts.Port4;
                    break;
                case EffectPort.P5:
                    effectPort = GpioPorts.Port5;
                    break;
                case EffectPort.P6:
                    effectPort = GpioPorts.Port6;
                    break;
                case EffectPort.P7:
                    effectPort = GpioPorts.Port7;
                    break;
            }

            GpioPorts effectPorts = _effectPortStates;

            if (on)
                effectPorts |= effectPort;
            else effectPorts = effectPorts & ~effectPort;

            if (_effectPortStates != effectPorts)
            {
                SetEffectPorts(effectPorts);
                _effectPortStates = effectPorts;
            }
        }

        private void SetXBeePorts(AuxPort port, DIOnMode mode)
        {
            XBeeIO.Instance.BeginRemotePortSet(SerialNumber, ShortAddress, (int) port, mode);
        }

        private void SetEffectPorts(GpioPorts ports)
        {
            SetupPorts();
            var gpioWrite = new GpioWrite(ports);
            XBeeIO.Instance.BeginRemoteTx(SerialNumber, gpioWrite.GetPacket(), HandleRemoteResponse);
        }

        public void SetupPorts()
        {
            if (!_portsConfigured)
            {
                var port1Conf = new PortConf1RegisterWrite();
                XBeeIO.Instance.BeginRemoteTx(SerialNumber, port1Conf.GetPacket(), HandleRemoteResponse);

                Thread.Sleep(100); // i know...

                var port2Conf = new PortConf2RegisterWrite();
                XBeeIO.Instance.BeginRemoteTx(SerialNumber, port2Conf.GetPacket(), HandleRemoteResponse);
                _portsConfigured = true;
            }
        }

        private void HandleRemoteResponse(TxStatusType statusType)
        {
            if (statusType == TxStatusType.Success)
                ConnectionState = ConnectionState.Connected;
            else if (statusType == TxStatusType.NoAcknowledgementReceived)
                ConnectionState = ConnectionState.Disconnected;
        }

        public void Initialize()
        {
            IsArmed = false;
            _portsConfigured = false;
            SetupPorts();

            /* cycle ports to force SC18IM700 into correct state */
            SetEffectPorts(GpioPorts.All);
            SetEffectPorts(GpioPorts.None);
        }

        public void SyncRemoteUnitName()
        {
            if (_address16 == null || _address64 == null)
                return;

            if (!string.IsNullOrWhiteSpace(Name))
                XBeeIO.Instance.SetNodeId(SerialNumber, ShortAddress, Name);
        }

        private void EffectsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (EffectTrack effect in e.NewItems)
                {
                    effect.PropertyChanged += EffectPropertyChanged;
                    DeconflictEffectPort(effect);
                    effect.Node = this;
                }
            }

            if (e.OldItems != null)
            {
                foreach (EffectTrack effect in e.OldItems)
                {
                    effect.PropertyChanged -= EffectPropertyChanged;
                    effect.Node = null;
                }
            }

            OnEffectsChanged();

            if (Effects.Count > MaxEffects)
                throw new InvalidOperationException(
                    string.Format("Nodes cannot support than {0} effects", MaxEffects));
        }

        private void EffectPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var effect = sender as EffectTrack;
            switch (e.PropertyName)
            {
                case "Port":
                    DeconflictEffectPort(effect);
                    break;
                case "IsHovered":
                    if (effect != null) 
                        IsHovered = effect.IsHovered;
                    break;
                case "IsHot":
                    OnPropertyChanged("IsHot");
                    break;
            }
        }

        public event EventHandler EffectsChanged;

        private void OnEffectsChanged()
        {
            if (EffectsChanged != null)
                EffectsChanged(this, EventArgs.Empty);
        }

        private void DeconflictEffectPort(EffectTrack effect)
        {
            EffectTrack collision = Effects.FirstOrDefault(e => (e.Port == effect.Port && e != effect));
            if (collision != null)
                collision.Port++;
        }

        protected void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }
}